Este relatório visa mostrar, por meio de uma espiral climática
animada, a mudança das anomalias da temperatura média global ao longo
dos anos. Com essa animação, será possível ver claramente o quão rica
pode ser uma análise de série temporal, principalmente se soubermos usar
as ferramentas corretas.
Quase todos os gráficos que vemos no dia a dia são estáticos, isto é, em
termos gerais podemos dizer que são apenas uma simples imagem que nos dá
informações úteis, mas muitas vezes grande parte do conhecimento que um
gráfico poderia passar é perdido, já que não podemos ver sua evolução,
apenas o resultado final, e isso é especialmente prejudicial em séries
temporais. Com a animação criada, será possível visualizar não somente o
ponto final, mas toda sua evolução. Em alguns segundos, teremos dados de
145 anos, ou 1740 meses!
Nas seções a seguir, serão detalhados os passos desde a escolha dos dados até o resultado final. A linguagem de programação utilizada foi R, devido sua intimidade com estatística e visualização de dados, além de pacotes gráficos específicos, além de outras ferramentas usadas para manipulação de dados.
A animação da espiral climática foi o formato escolhido por ser uma representação fidedigna, progressiva e já bem conceituada no mundo científico, originalmente proposta e criada por Ed Hawkins, do National Centre for Atmospheric Science da Universidade de Reading, no Reino Unido, em 2016.
Os dados utilizados neste trabalho são um dataset disponibilizado oficial e publicamente pela NASA (National Aeronautics and Space Administration), em suas dependências online, cujo download pode ser feito por este link. A base de dados nos dá informações acerca das anomalias de temperatura média globais desde 1880 até 2025, tendo como base de comparação a média dos 1951 a 1980, de modo que, como o planeta tem se tornado cada vez mais quente, é natural que nos anos anteriores a essa linha base, tenhamos tido “anomalias negativas”, em outras palavras, diferenças de temperatura média para baixo d
Abaixo, podemos ver as 12 últimas linhas do dataframe, referentes aos meses do ano de 2025.
| Year | Month | Temperature | Month number | theta | r |
|---|---|---|---|---|---|
| 2025 | Jan | 1.38 | 1 | 0.0000000 | 2.30 |
| 2025 | Feb | 1.26 | 2 | 0.5235988 | 2.18 |
| 2025 | Mar | 1.36 | 3 | 1.0471976 | 2.28 |
| 2025 | Apr | 1.23 | 4 | 1.5707963 | 2.15 |
| 2025 | May | 1.08 | 5 | 2.0943951 | 2.00 |
| 2025 | Jun | 1.05 | 6 | 2.6179939 | 1.97 |
| 2025 | Jul | 1.02 | 7 | 3.1415927 | 1.94 |
| 2025 | Aug | 1.16 | 8 | 3.6651914 | 2.08 |
| 2025 | Sep | 1.25 | 9 | 4.1887902 | 2.17 |
| 2025 | Oct | 1.19 | 10 | 4.7123890 | 2.11 |
| 2025 | Nov | 1.21 | 11 | 5.2359878 | 2.13 |
| 2025 | Dec | 1.05 | 12 | 5.7595865 | 1.97 |
Essa seção é divididas em duas sub-seções
Além disso, o código mostrado aqui contém a expressão “…caminho” nas partes de leitura de um dataset ou de salvamento do mesmo, de modo que o código possa ser apenas copiado e colocado em uso, de acordo com a organização e diretório necessários.
A limpeza e preparação dos dados foi extremamente simples, visto que o dataset disponibilizado pela NASA já estava muito bem organizado, de modo que foram feitas apenas algumas mudanças.
O primeiro pensamento de um estatístico ao receber um banco de dados para tratamento, deve ser sempre “Quais informações são importantes nesses dados, e o que posso eliminar ?”, de modo que primeiramente foram removidas variáveis que não seriam úteis para a análise em questão, como informações da temperatura por trimestre específico.
Além disso, o dataset inicialmente estava em formato largo, o que
dificultaria a análise da série temporal, por isso foi necessária
transformá-lo para um formato longo, por meio da função
pivot_longer(), do pacote {tidyr}.
Os meses foram organizados em ordem correta, visto que havia o risco que o R colocá-los em ordem alfabética, e após isso, cada mês se tornou um ângulo específico.
Por último, as temperaturas se tornaram o valor do raio da espiral, e cada raio \(r_{i}\) foi calculado a partir da soma de cada temperatura a uma constante, garantindo que não tenhamos valores negativos, que acabariam colapsando no centro da espiral devido ao método matemático adotado, como será explicado a seguir
Abaixo, temos o código utilizado:
library(tidyr)
library(dplyr)
#IMPORTACAO
df_wide <- read.csv("...caminho/GLB.Ts+dSST.csv", skip = 1)
df_wide <- df_wide[-c(14:19)]
#WIDE TO LONG
meses <- colnames(df_wide[-1])
df_long <- df_wide %>%
pivot_longer(
cols = meses,
names_to = "Month",
values_to = "Temp"
)
#ORGANIZANDO MESES NA ORDEM CERTA
df_long <- df_long %>%
mutate(
Month = factor(Month, levels = meses)
)
#TRANSFORMANDO OS MESES EM ANGULOS
df_long <- df_long %>%
mutate(
mes_numerico = as.numeric(Month),
theta = 2*pi*(mes_numerico - 1)/12
)
#GARANTINDO QUE NÃO TEREMOS VALORES NEGATIVOS EM COORDENADAS POLARES
minimo <- min(df_long$Temp)
const <- abs(minimo) + 0.1
#CRIANDO A VARIAVEL RAIO
df_long <- df_long %>%
mutate(
r = Temp + const
)
#SALVANDO O DATAFRAME
write.table(df_long, "...caminho/climate_dataframe.txt", sep = ",", row.names = F)
O pacote {gganimate} é basicamente uma extensão do {ggplot2}, de modo que antes de renderizarmos uma animação, é necessário criar um gráfico estático. Assim, ao criarmos o gráfico estático, precisamos pensar, de que modo posso criar uma espiral que fecha um ciclo a cada ano — passando por cada mês, passando por toda a série temporal ?
Se o gganimate é apenas uma extensão do ggplot, como existe uma espiral, e não uma circunferência exata ? A definição matemática é simples, o raio \(r\) depende não só do mês ou do ano, mas simultaneamente do par \((ano,\ mês)\), de modo que cada vez que a espiral fecha um ciclo, se inicia um novo ano. Assim, podemos notar a tendência de aumento das anomalias de temperatura conforme a espiral tende “se abrir” cada vez mais, isto é, a cada ciclo, o raio aumenta.
O lógica utilizada foi um método simples, mas bem conhecido no cálculo, chamado coordenadas polares. Em resumo, ele transforma números para um formato radial, a partir da seguinte fórmula:
\[ x = rcos(\theta) \newline y = rsen(\theta) \]
A partir dessa lógica, temos a temperatura transformada no raio r, como descrito anteriormente, e os meses sendo ângulos específicos, ângulos esses que foram calculados dividindo uma circunferência de \(12\pi\ rad\) em 12 partes, a partir da fórmula abaixo, de modo que cada \(\theta\) representa o ângulo de um mês:
\[ \theta_m = \frac{2\pi}{12}\times(m-1),\ m = 1, ..., 12 \]
O \(m-1\) na fórmula serve para fazer com que o primeiro mês, Janeiro, comece em 0 radianos, de modo que o último, Dezembro, não fique em \(2\pi\), causando uma sobreposição de Janeiro e Dezembro, visto que \(0\) e \(2\pi\) são equivalentes em coordenadas polares.
Vale comentar que usar esse método para evitar a sobreposição também cria outro problema adjacente, que é um vão entre Dezembro e Janeiro, visto que o ggplot2, mais especificamente, o geom_path não fecha o ciclo perfeitamente, já que em \(\theta = 2\pi\) teoricamente não havia nada, e para resolver isso, basta repetir o raio \(r_{Jan}\) ao final, garantindo continuidade sem redundância dos dados.
Aqui, apresentarei um bloco de código e, logo abaixo, a devida explicação.
dados <- read.table("...caminho/climate_dataframe.txt", header = T, sep = ",")
library(tidyverse)
library(ggplot2)
library(gganimate)
library(gifski)
library(viridis)
Aqui, apenas foram carregados os pacotes necessários, e o dataframe utilizado, que foi criado na parte de Tratamento dos dados!
const <- abs(min(dados$Temp)) + 0.1
media_51_80 <- dados %>%
filter(Year >= 1951, Year <= 1980) %>%
summarise(media = mean(Temp, na.rm = TRUE)) %>%
pull(media)
r_media_51_80 <- media_51_80 + const
Definição de uma constante positiva que desloca levemente todas os
valores, com o inuito de evitar valores negativos no raio.
Além disso, foi definida uma média dos anos 1951-1980 pare servir de
base, que aparecerá como uma linha branca na animação.
dados <- dados |>
group_by(Year) |>
bind_rows(
dados |>
filter(theta == 0) |>
mutate(theta = 2*pi)
) |>
ungroup()
Aqui, temos um bloco de código que serve para corrigir o erro apontado na parte matemática. Esse código replica os dados do mês de Janeiro de cada ano e reposiciona em \(\theta = 2\pi\).
meses_chave <- tibble(
theta = 2*pi*((0:11) + 0.5)/12
)
Nessa parte, cada mês é convertido em um ângulo, como explicado anteriormente, a partir da fórmula descrita.
nomes_meses_chave <- tibble(
Month = c("Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"),
theta = 2*pi*(0:11)/12,
r = max(dados$r) + 0.2
)
Esse código apenas cria rótulos para os meses, que serão colocados ao redor da espiral. O raio é um pouco maior do que o valor próprio dele, sendo somado a 0.15m para garantir que o texto de cada mês não se sobreponha à própria espiral
dados <- dados %>%
arrange(Year, theta)
Como o nome sugere, apenas ordena os dados por ano e ângulo, para que o ggplot seja capaz de conectar os pontos corretamente ao plotar a série temporal.
anim <- ggplot(dados, aes(
x = theta,
y = r,
group = Year,
colour = Year
))+
No começo do código do ggplot, definimos o eixo x como sendo o ângulo em radianos e o eixo y como o raio, e o agrupamento pela variável Year garante que cada ciclo seja um ano inteiro independentemente, e anos distintos não acabem se conectando de forma caótica
geom_path(alpha = 0.6, linewidth = 0.4) +
O geom_path() desenha as curva de cada ano na espiral,
conectando os meses na ordem pré-definida, por causa do funcionamento
dessa função é que foi necessário fazer a duplicata do mês de Janeiro,
para que não sobrasse um vão. os argumentos alpha e
linewidth serve, respectivamente, para controlar a
opacidade e a espessura da linha.
geom_hline(yintercept = r_media_51_80,
colour = "white",
linewidth = 0.8,
alpha = 0.8) +
Essa parte do código adiciona um “ciclo” constante, que é a média dos anos 1951 a 1980, para servir como referência visual para comparação.
geom_segment(
data = meses_chave,
aes(
x = theta,
xend = theta,
y = min(dados$r),
yend = max(dados$r)
),
inherit.aes = F,
colour = "white",
linewidth = 0.3,
alpha = 0.4
)+
Apenas um detalhe visual, mas que facilita bastante a visualização e interpretação. O código acima desenha linhas que delimitam a área que representa um mês e o separa dos meses vizinhos.
geom_text(
data = nomes_meses_chave,
aes(
x = theta,
y = r,
label = Month
),
inherit.aes = F,
colour = "white",
size = 4,
fontface = "bold"
)+
Aqui é a parte citada anteriormente, que adiciona os nomes dos meses ao redor da espiral, porém um pouco mais distante dela, o valor máximo dos dados somado a 0.2 (como visto na subseção ‘Preparação dos rótulos dos meses’!).
coord_polar(start = -pi/12, direction = 1)+
Serve para transformar o sistema cartesiano em polar, convertendo os eixos comuns x e y em raio, o argumento start garante que Janeiro fique centrado logo acima, de modo que visualmente agradável e intuitivo.
scale_colour_viridis_c(option = "magma",
begin = 0.1,
end = 0.95)+
Parte do código que visa tornar a animação inteligível para pessoas com daltonismo, visto que, em média, 1 a cada 20 pessoas tenha alguma dificuldade para dicernir cores.
theme_void() +
Essa única linha pode alterar bastante o visual do gráfico, há várias opções possíveis, entretanto o tama ‘void’, que significa vazio, remove os eixos e outros elementos gráficos que, nesse caso, apenas poluiriam o o visual final, visto que neste caso não precisamos de uma leitura quantitativa exata, mas sim de uma representação abstrata ao longo do tempo.
theme(
plot.background = element_rect(fill = "black", color = NA),
panel.background = element_rect(fill = "black", color = NA),
plot.title = element_text(color = "white", size = 16, face = "bold"),
plot.subtitle = element_text(color = "gray80", size = 11),
legend.position = "right",
legend.title = element_text(color = "white"),
legend.text = element_text(color = "white"),
plot.margin = margin(10, 10, 10, 10)
)+
Apenas define estilos visuais do gráfico.
labs(
title = "Anomalias de Temperatura Globais",
subtitle = "Anomalias mensais da temperatura média global",
colour = "Year"
) + transition_reveal(Year)
Define títulos e legenda. Além disso, a parte
transition_reveal(Year) faz com que haja uma progressão,
isto é, os dados sejam mostrados um após o outro, ordenados pelos anos,
é isto que faz a animação da espiral fazer sentido.
animacao <- animate(anim,
nframes = 450,
fps = 30,
width = 800,
height = 800,
renderer = gifski_renderer()
)
anim_save("...caminho/espiral_clima.gif",
animation = animacao)
Parte final da animação. É aqui que o {gganimate} transforma o gráfico estático do {ggplot} em algo dinâmico e progressivo. A função animate nos permite definir a quantidade total de quadros (nframes), sendo que quando maior esse, maior tende a ser a duração da animação resultante. Já fps nos permite definir a quantidade de quadros por segundo, que torna a animação mais fluida quanto maior for, mas ao mesmo tempo também gera um arquivo mais pesado.
Abaixo, podemos ver a animação resultante, além do último frame da mesma.
Podemos ver claramente a progressão da série temporal conforme as cores mudam de mais frias para mais quentes, o que evidencia o aumento das anomalias de temperatura média globais.
Como em todo trabalho que envolve código, sempre há erros de digitação (sintaxe) ou até mesmo de lógica, e houveram alguns especialmente importantes, que destaco abaixo
Inicialmente, a ideia era manter apenas 4 meses escritos ao redor da espiral, de forma trimestral, mas depois decidi manter todos os meses. Porém, em um sistema de coordenadas polares, \(0\) e \(2\pi\) representam o mesmo ângulo, por isso Janeiro e Dezembro ficaram sobrepostos, por isso resolvi o problema deslocando os meses em 0.5 (meio mês), separando os nomes sobrepostos.
Resolver o problema anterior gerou um novo infortúnio. Com os meses
separados, existia um vão entre Dezembro e Janeiro! Um ausência completa
de dados. Isso ocorreu porque o ggplot — mais especificamente, a função
geom_path() liga um dado no próximo, porém como Janeiro
começa em \(0\ rad\) , Dezembro termina
em \(\frac{11\pi}{6}\ rad\), então a
forma de resolver foi “duplicar” os dados de Janeiro de cada ano e
colocar logo após Dezembro, em \(2\pi\), solucionando o problema. Foi uma
confusão por parte do ggplot, que gerou a necessidade de uma adaptação
na parte teórica do cálculo.
Um erro simples que eu normalmente não registraria aqui, mas tomou
certo tempo até entender o que o estava causando. Ao renderizar a
animação por meio da função animate do {gganimate},
centenas de imagens ficavam salvas no meu notebook, porém nenhuma
animação era criada. Tentei alterar várias coisas, tanto com relação ao
diretório, como a própria função. Tentei conseguir um diagnóstico usando
class(animacao), e o resultado era “function”, sendo que
deveria ser algo como “gif”. Apenas após bons minutos que encontrei o
erro, e era uma simples falta de parênteses. Eu havia escrito
gifski_renderer, e não gifski_renderer().
Aqui foi um problema principalmente estético. O fundo do gráfico foi definido como preto, porém o texto do Título e legenda também, por isso ficaram invisíveis, então a solução foi mudar a cor do texto.
Além disso, também houve complicações com posicionamento, visto que o título original havia ficado longo demais e vazado para fora da área do gráfico. Não tendo sido suficiente, criei uma confusão tentando acertar as dimensões do gráfico (para a esquerda e direita, e para cima e para baixo).
Aqui gostaria de deixar algumas ideias que poderiam ser realizadas se houvesse mais tempo para completar esse trabalho.
Como temos uma espiral em 2d cujo raio se expande, penso que teríamos algo próximo de um parabolóide em 3d, o que mostraria bem a progressão da série temporal. Abaixo podemos ver um protótipo.
Uma ideia interessante seria organizar o código de modo que ele próprio atualizassa a URL da fonte dos dados importasse o dataset mais recente, criando assim uma nova animação sem precisar reescrever um código inteiro
Seria possível encontrar outros datasets com dados sobre a situação climática global e usá-los em paralelo com este, chegando assim a novas respostas e conclusões, além de gráficos e animações diferentes.
COOKSON, Alex. Building an animation step by step with gganimate. Disponível em: https://www.alexcookson.com/post/2020-10-18-building-an-animation-step-by-step-with-gganimate/. Acesso em: 29 jan. 2026.
NATIONAL AERONAUTICS AND SPACE ADMINISTRATION. GISTEMP: Global Surface Temperature Analysis. Disponível em: https://data.giss.nasa.gov/gistemp/. Acesso em: 29 jan. 2026.
NATIONAL AERONAUTICS AND SPACE ADMINISTRATION. Climate Spiral 1880–2022. Disponível em: https://science.nasa.gov/resource/video-climate-spiral-1880-2022/. Acesso em: 29 jan. 2026.
NUNES, Marcus. Daltonismo e paletas de cores inclusivas no R. Disponível em: https://marcusnunes.me/posts/daltonismo-paletas-de-cores-inclusivas-no-r/. Acesso em: 29 jan. 2026.
RANYDC. R Markdown themes. Disponível em: https://rpubs.com/ranydc/rmarkdown_themes. Acesso em: 29 jan. 2026.